using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;

namespace Comms.Drt {
    public class Client : IDisposable {
        public volatile bool IsDisposed;

        public Alarm Alarm;

        public Queue<ServerTickMessage> TickMessages = new();

        public int MaxAllowedStep;

        public double NextTickExpectedTime;

        public double LastStepTime;

        public MessageSerializer MessageSerializer;

        public float TickDurationField;

        public int StepsPerTickField;

        public int GameIDField;

        public int ClientIDField;

        public int StepField;

        public int GameTypeID => MessageSerializer.GameTypeID;

        public float TickDuration {
            get {
                CheckNotDisposed();
                return TickDurationField;
            }
            set => TickDurationField = value;
        }

        public int StepsPerTick {
            get {
                CheckNotDisposed();
                return StepsPerTickField;
            }
            set => StepsPerTickField = value;
        }

        public float StepDuration => TickDuration / StepsPerTick;

        public DesyncDetectionMode DesyncDetectionMode { get; set; }

        public int DesyncDetectionPeriod { get; set; }

        public int? DesyncDetectedStep { get; set; }

        public int GameID {
            get {
                CheckNotDisposed();
                return GameIDField;
            }
            set => GameIDField = value;
        }

        public int ClientID {
            get {
                CheckNotDisposed();
                return ClientIDField;
            }
            set => ClientIDField = value;
        }

        public int Step {
            get {
                CheckNotDisposed();
                return StepField;
            }
            set => StepField = value;
        }

        public Peer Peer { get; set; }

        public object Lock => Peer.Lock;

        public IPEndPoint Address => Peer.Address;

        public bool IsConnecting => Peer.ConnectingTo != null;

        public bool IsConnected => Peer.ConnectedTo != null;

        public ClientSettings Settings { get; } = new();

        public float StalledTime {
            get {
                lock (Peer.Lock) {
                    if (IsConnected) {
                        return (float)Math.Max(Comm.GetTime() - (LastStepTime + StepDuration), 0.0);
                    }
                    return 0f;
                }
            }
        }

        public event Action<GameCreatedData> GameCreated;

        public event Action<GameJoinedData> GameJoined;

        public event Action<ConnectRefusedData> ConnectRefused;

        public event Action<ConnectTimedOutData> ConnectTimedOut;

        public event Action<GameStateRequestData> GameStateRequest;

        public event Action<GameDesyncStateRequestData> GameDesyncStateRequest;

        public event Action<GameDescriptionRequestData> GameDescriptionRequest;

        public event Action<GameStepData> GameStep;

        public event Action<DisconnectedData> Disconnected;

        public event Action<Exception> Error;

        public event Action<string> Debug;

        public Client(int gameTypeID, int localPort = 0) : this(gameTypeID, new UdpTransmitter(localPort)) { }

        public Client(int gameTypeID, ITransmitter transmitter) {
            if (transmitter == null) {
                throw new ArgumentNullException("transmitter");
            }
            MessageSerializer = new MessageSerializer(gameTypeID);
            Peer = new Peer(transmitter);
            Peer.Error += delegate(Exception e) {
                if (!(e is MalformedMessageException)) {
                    InvokeError(e);
                }
            };
            Peer.PeerDiscoveryRequest += delegate { };
            Peer.ConnectAccepted += delegate(PeerPacket p) {
                if (!IsDisposed) {
                    Message message = MessageSerializer.Read(p.Bytes, p.PeerData.Address);
                    if (!(message is ServerCreateGameAcceptedMessage message2)) {
                        if (!(message is ServerJoinGameAcceptedMessage message3)) {
                            throw new ProtocolViolationException($"Unexpected message type {message.GetType()}.");
                        }
                        Handle(message3);
                    }
                    else {
                        Handle(message2);
                    }
                }
            };
            Peer.ConnectRefused += delegate(Packet p) {
                if (!IsDisposed) {
                    Message message4 = MessageSerializer.Read(p.Bytes, p.Address);
                    if (!(message4 is ServerConnectRefusedMessage message5)) {
                        throw new ProtocolViolationException($"Unexpected message type {message4.GetType()}.");
                    }
                    Handle(message5, p.Address);
                }
            };
            Peer.ConnectTimedOut += delegate(IPEndPoint p) {
                if (!IsDisposed) {
                    ConnectTimedOut?.Invoke(new ConnectTimedOutData { Address = p });
                }
            };
            Peer.DataMessageReceived += delegate(PeerPacket p) {
                if (!IsDisposed) {
                    Message message6 = MessageSerializer.Read(p.Bytes, p.PeerData.Address);
                    if (!(message6 is ServerStateRequestMessage message7)) {
                        if (!(message6 is ServerDesyncStateRequestMessage message8)) {
                            if (!(message6 is ServerGameDescriptionRequestMessage message9)) {
                                if (!(message6 is ServerTickMessage message10)) {
                                    throw new ProtocolViolationException($"Unexpected message type {message6.GetType()}.");
                                }
                                Handle(message10);
                            }
                            else {
                                Handle(message9);
                            }
                        }
                        else {
                            Handle(message8);
                        }
                    }
                    else {
                        Handle(message7);
                    }
                }
            };
            Peer.Disconnected += delegate {
                if (!IsDisposed) {
                    HandleDisconnected();
                }
            };
        }

        public void Dispose() {
            lock (Peer.Lock) {
                if (IsDisposed) {
                    return;
                }
                IsDisposed = true;
            }
            Alarm.Dispose();
            Peer.Dispose();
        }

        public void Start() {
            lock (Peer.Lock) {
                CheckNotDisposed();
                if (Alarm != null) {
                    throw new InvalidOperationException("Client is already started.");
                }
                Peer.Start();
                Alarm = new Alarm(AlarmFunction);
                Alarm.Error += delegate(Exception e) { InvokeError(e); };
                Alarm.Set(0.0);
            }
        }

        public void CreateGame(IPEndPoint serverAddress, byte[] gameDescriptionBytes, string clientName = null) {
            Peer.Connect(
                serverAddress,
                MessageSerializer.Write(new ClientCreateGameRequestMessage { ClientName = clientName, GameDescriptionBytes = gameDescriptionBytes })
            );
        }

        public void JoinGame(IPEndPoint serverAddress, int gameID, byte[] joinRequestBytes = null, string clientName = null) {
            Peer.Connect(
                serverAddress,
                MessageSerializer.Write(
                    new ClientJoinGameRequestMessage { GameID = gameID, JoinRequestBytes = joinRequestBytes, ClientName = clientName }
                )
            );
        }

        public void LeaveGame() {
            Peer.Disconnect();
        }

        public void AcceptJoinGame(int clientID) {
            lock (Peer.Lock) {
                CheckNotDisposedAndConnected();
                Peer.SendDataMessage(
                    Peer.ConnectedTo,
                    DeliveryMode.Reliable,
                    MessageSerializer.Write(new ClientJoinGameAcceptedMessage { ClientID = clientID })
                );
            }
        }

        public void RefuseJoinGame(int clientID, string reason) {
            lock (Peer.Lock) {
                CheckNotDisposedAndConnected();
                Peer.SendDataMessage(
                    Peer.ConnectedTo,
                    DeliveryMode.Reliable,
                    MessageSerializer.Write(new ClientJoinGameRefusedMessage { ClientID = clientID, Reason = reason })
                );
            }
        }

        public void SendInput(byte[] inputBytes) {
            lock (Peer.Lock) {
                CheckNotDisposedAndConnected();
                Peer.SendDataMessage(
                    Peer.ConnectedTo,
                    DeliveryMode.Reliable,
                    MessageSerializer.Write(new ClientInputMessage { InputBytes = inputBytes })
                );
            }
        }

        public void SendState(int step, byte[] stateBytes) {
            lock (Peer.Lock) {
                CheckNotDisposedAndConnected();
                Peer.SendDataMessage(
                    Peer.ConnectedTo,
                    DeliveryMode.Reliable,
                    MessageSerializer.Write(new ClientStateMessage { Step = step, StateBytes = stateBytes })
                );
            }
        }

        public void SendDesyncState(int step, byte[] stateBytes, bool isDeflated) {
            lock (Peer.Lock) {
                CheckNotDisposedAndConnected();
                Peer.SendDataMessage(
                    Peer.ConnectedTo,
                    DeliveryMode.Reliable,
                    MessageSerializer.Write(new ClientDesyncStateMessage { Step = step, StateBytes = stateBytes, IsDeflated = isDeflated })
                );
            }
        }

        public void SendStateHashes(int firstHashStep, ushort[] stateHashes) {
            lock (Peer.Lock) {
                CheckNotDisposedAndConnected();
                Peer.SendDataMessage(
                    Peer.ConnectedTo,
                    DeliveryMode.Reliable,
                    MessageSerializer.Write(new ClientStateHashesMessage { FirstHashStep = firstHashStep, Hashes = stateHashes })
                );
            }
        }

        public void SendGameDescription(byte[] gameDescriptionBytes) {
            lock (Peer.Lock) {
                CheckNotDisposedAndConnected();
                Peer.SendDataMessage(
                    Peer.ConnectedTo,
                    DeliveryMode.Unreliable,
                    MessageSerializer.Write(new ClientGameDescriptionMessage { Step = Step, GameDescriptionBytes = gameDescriptionBytes })
                );
            }
        }

        public void CheckNotDisposed() {
            if (IsDisposed) {
                throw new ObjectDisposedException("Client");
            }
        }

        public void CheckNotDisposedAndConnected() {
            CheckNotDisposed();
            if (!IsConnected) {
                throw new InvalidOperationException("Not connected.");
            }
        }

        [Conditional("DEBUG")]
        public void InvokeDebug(string format, params object[] args) {
            if (Debug != null) {
                Debug?.Invoke(string.Format(format, args));
            }
        }

        public void InvokeError(Exception error) {
            Error?.Invoke(error);
        }

        public void InitializeConnection(IEnumerable<ServerTickMessage> tickMessages) {
            MaxAllowedStep = 0;
            LastStepTime = Comm.GetTime() - StepDuration;
            TickMessages.Clear();
            if (tickMessages == null) {
                return;
            }
            foreach (ServerTickMessage tickMessage in tickMessages) {
                TickMessages.Enqueue(tickMessage);
            }
        }

        public double GetStepWaitTime(double time) {
            if (TickMessages.Count > 0) {
                ServerTickMessage serverTickMessage = TickMessages.Last();
                MaxAllowedStep = (TickMessages.Last().Tick + 1) * StepsPerTick;
                NextTickExpectedTime = serverTickMessage.ReceivedTime + TickDuration;
            }
            if (Step >= MaxAllowedStep) {
                return double.PositiveInfinity;
            }
            int num = MaxAllowedStep - Step;
            double num2 = NextTickExpectedTime - time;
            double num3 = num * StepDuration - num2;
            return Settings.SafetyLag - num3;
        }

        public void AlarmFunction() {
            try {
                lock (Peer.Lock) {
                    if (IsDisposed) {
                        return;
                    }
                    double num = double.PositiveInfinity;
                    if (IsConnected) {
                        while (true) {
                            double time = Comm.GetTime();
                            num = GetStepWaitTime(time);
                            if (!(num <= 0.0)) {
                                break;
                            }
                            ServerTickMessage serverTickMessage;
                            if (Step % StepsPerTick == 0) {
                                serverTickMessage = TickMessages.Dequeue();
                                if (serverTickMessage.Tick != Step / StepsPerTick) {
                                    throw new Exception($"Wrong tick message, expected {Step / StepsPerTick} got {serverTickMessage.Tick}.");
                                }
                            }
                            else {
                                serverTickMessage = null;
                            }
                            int step = Step + 1;
                            Step = step;
                            LastStepTime = time;
                            GameStepData obj = CreateGameStepData(serverTickMessage);
                            try {
                                GameStep?.Invoke(obj);
                            }
                            catch (Exception obj2) {
                                Error?.Invoke(obj2);
                            }
                        }
                    }
                    Alarm.Set(num);
                }
            }
            catch (Exception error) {
                InvokeError(error);
            }
        }

        public GameStepData CreateGameStepData(ServerTickMessage tickMessage) {
            GameStepData result;
            if (tickMessage != null) {
                List<GameStepData.JoinData> list = [];
                List<GameStepData.LeaveData> list2 = [];
                List<GameStepData.InputData> list3 = [];
                foreach (ServerTickMessage.ClientTickData clientsTickDatum in tickMessage.ClientsTickData) {
                    if (clientsTickDatum.JoinBytes != null) {
                        list.Add(
                            new GameStepData.JoinData {
                                ClientID = clientsTickDatum.ClientID,
                                Address = clientsTickDatum.JoinAddress,
                                JoinRequestBytes = clientsTickDatum.JoinBytes
                            }
                        );
                    }
                    else if (clientsTickDatum.Leave) {
                        list2.Add(new GameStepData.LeaveData { ClientID = clientsTickDatum.ClientID });
                    }
                    else {
                        if (clientsTickDatum.InputsBytes == null) {
                            continue;
                        }
                        foreach (byte[] inputsByte in clientsTickDatum.InputsBytes) {
                            list3.Add(new GameStepData.InputData { ClientID = clientsTickDatum.ClientID, InputBytes = inputsByte });
                        }
                    }
                }
                result = default;
                result.Step = Step;
                result.Joins = list.ToArray();
                result.Leaves = list2.ToArray();
                result.Inputs = list3.ToArray();
                return result;
            }
            result = default;
            result.Step = Step;
            result.Joins = [];
            result.Leaves = [];
            result.Inputs = [];
            return result;
        }

        public void Handle(ServerCreateGameAcceptedMessage message) {
            GameID = message.GameID;
            ClientID = 0;
            TickDuration = message.TickDuration;
            StepsPerTick = message.StepsPerTick;
            DesyncDetectionMode = message.DesyncDetectionMode;
            DesyncDetectionPeriod = message.DesyncDetectionPeriod;
            Step = 0;
            InitializeConnection(null);
            GameCreated?.Invoke(new GameCreatedData { CreatorAddress = message.CreatorAddress });
        }

        public void Handle(ServerJoinGameAcceptedMessage message) {
            GameID = message.GameID;
            ClientID = message.ClientID;
            TickDuration = message.TickDuration;
            StepsPerTick = message.StepsPerTick;
            DesyncDetectionMode = message.DesyncDetectionMode;
            DesyncDetectionPeriod = message.DesyncDetectionPeriod;
            Step = message.Step;
            InitializeConnection(message.TickMessages);
            GameJoined?.Invoke(new GameJoinedData { Step = message.Step, StateBytes = message.StateBytes });
        }

        public void Handle(ServerConnectRefusedMessage message, IPEndPoint address) {
            ConnectRefused?.Invoke(new ConnectRefusedData { Address = address, Reason = message.Reason });
        }

        public void Handle(ServerStateRequestMessage message) {
            GameStateRequest?.Invoke(default);
        }

        public void Handle(ServerDesyncStateRequestMessage message) {
            GameDesyncStateRequest?.Invoke(new GameDesyncStateRequestData { Step = message.Step });
        }

        public void Handle(ServerGameDescriptionRequestMessage message) {
            GameDescriptionRequest?.Invoke(default);
        }

        public void Handle(ServerTickMessage message) {
            TickMessages.Enqueue(message);
            if (!DesyncDetectedStep.HasValue
                && message.DesyncDetectedStep.HasValue) {
                DesyncDetectedStep = message.DesyncDetectedStep;
            }
            Alarm.Set(0.0);
        }

        public void HandleDisconnected() {
            Disconnected?.Invoke(default);
        }
    }
}